<?php

/*
 * Copyright (C) 2009 - 2011 Pham Cong Dinh
 *
 * This file is part of Spica.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 3 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

// namespace spica\core\service;

/**
 * Set of methods considering manipulations of form submissions
 * and user input validation.
 *
 * The model layer ("M") of MVC is broken into an application tier, a domain tier,
 * and an infrastructure tier. The infrastructure tier is used to retrieve and
 * store data. The domain tier is where the business knowledge or expertise is.
 * The application tier is responsible for coordinating the infrastructure and
 * domain tiers to make a useful application.
 *
 * Form is part of domain tier.
 *
 * This form class does not include any validator implicitly. I believe that it is
 * developer's job.
 *
 * @category   spica
 * @package    core
 * @subpackage service
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.2
 * @since      March 21, 2009
 * @copyright  Pham Cong Dinh (http://www.phpvietnam.net)
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: FormService.php 1869 2011-01-07 18:55:25Z pcdinh $
 */
abstract class SpicaFormService
{
    /**
     * Sesion key to store form's request-based secure token key.
     */
    const SECURE_SESSION_KEY = '__form_secure_token__';

    /**
     * Html hidden input name.
     */
    const SECURE_INPUT_NAME = '__token__';

    /**
     * Form title
     *
     * @var array
     */
    protected $_title;

    /**
     * Form header when violation message is available.
     *
     * @var string
     */
    protected $_violationHead = 'Please correct the errors noted below.';

    /**
     * Text block for form input guideline that appears beneath form title.
     *
     * @var string
     */
    protected $_guide;

    /**
     * Message after form data is processed without any error.
     *
     * @var string
     */
    public $processMessage;

    /**
     * List of notices that are added to show up in the top of the form.
     *
     * @var array
     */
    protected $_notices = array();

    /**
     * Validation tasks
     *
     * @var array Array[input, validator]
     */
    protected $_tasks = array();

    /**
     * Stack of validators that produces violation messages associated to each tested field value.
     *
     * @var array Array(field::SpicaValidator[])
     */
    protected $_violation = array();

    /**
     * The prepopulated data before user submits form.
     *
     * @var array
     */
    protected $_prepopulatedData = array();

    /**
     * Is form used in update context?
     *
     * @var bool true if form is used to update data, false if form is used to created data.
     */
    protected $_updateContext = false;

    /**
     * Enable cross-site request forgery protection
     *
     * Config variable
     *
     * @var bool
     */
    protected $_csrfProtectionEnabled = false;

    /**
     * Is the form processed successfully?
     *
     * @var bool
     */
    protected $_processed = false;

    /**
     * Constructs an object of <code>SpicaFormService</code>.
     *
     * @param bool $updateContext true if form is used to update data, false if form is used to created data.
     */
    public function __construct($updateContext = false)
    {
        $this->_updateContext = $updateContext;
        // Initialize form hooks
        $this->initialize();

        // Form is submitted via POST
        if (false === empty($_POST))
        {
            $this->afterPost();
            $this->buildValidation();
        }
        else
        {
            $this->beforePost();
        }
    }

    /**
     * Pre-populates form data.
     *
     * @param array $data Pre-populated data which is array whose key is form "name" attribute and value is form "value" attribute
     */
    public function prepopulate($data)
    {
        $this->_prepopulatedData = $data;
    }

    /**
     * Hooks to perform actions each time form is created.
     */
    public function initialize()
    {
        // no-op
    }

    /**
     * Hooks to perform actions before POST event happens
     * ($_POST is not populated with user input). This hook can be used for
     *
     * + populating default data
     * + firing up some events
     */
    public function beforePost()
    {
        // no-op
    }

    /**
     * Hooks to perform actions after POST event happens ($_POST is populated
     * with user input). This method is executed before SpicaFormService#initialize()
     */
    public function afterPost()
    {
        // no-op
    }

    /**
     * Declares form specific validation rules.
     *
     * The order of validator registration is important.
     */
    public abstract function buildValidation();

    /**
     * Binds a rule into the form to ensure that your users enter data properly.
     *
     * Validation rules restrict what users can enter in a given field,
     * and also help ensure that your users enter the proper types
     * or amounts of data.
     *
     * @throws InvalidArgumentException
     * @param  string|array     $inputName
     * @param  SpicaValidatable $validator
     */
    public function setRule($inputName, $validator)
    {
        if (null === $validator)
        {
            throw new InvalidArgumentException('A validator "'.$inputName.'" must be set.');
        }

        $this->_tasks[] = array(
            'input'     => $inputName,
            'validator' => $validator,
        );
    }

    /**
     * Hook to perform action before SpicaFormService's validate() does its jobs.
     * If this method returns false, validation will not pass.
     *
     * @return bool Returns true when pre-conditions are met and validate() can continue.
     *              Returns false when pre-conditions are not met and validate() can not continue.
     */
    public function beforeValidate()
    {
        return true;
    }

    /**
     * Hook to perform action after SpicaFormService's validate() does its jobs.
     * If this method returns false, validation will not pass.
     *
     * @return bool
     */
    public function afterValidate()
    {
        return true;
    }

    /**
     * Validates user input against set of rules.
     *
     * @throws SpicaValidatorException if there is any violation
     * @throws InvalidArgumentException if there is any error when registering validator
     * @return null|true returns null if there is no validation rule to execute, true otherwise
     */
    public function validate()
    {
        if (false === $this->beforeValidate())
        {
            throw new SpicaValidatorException();
        }

        if (true === empty($this->_tasks))
        {
            return null;
        }

        // Check security requirement first
        if (true === $this->_csrfProtectionEnabled && false === $this->validateSecurityToken())
        {
            throw new SpicaValidatorException();
        }

        foreach ($this->_tasks as $task)
        {
            if (false === isset($_POST[$task['input']]))
            {
                $_POST[$task['input']] = null;
            }

            if (false === $task['validator']->isValid($_POST[$task['input']]))
            {
                // A field can be validated by many validators so it can get many
                // violation messages.
                $this->_violation[$task['input']][] = $task['validator'];
            }
        }

        if (false === empty($this->_violation))
        {
            throw new SpicaValidatorException();
        }

        if (false === $this->afterValidate())
        {
            throw new SpicaValidatorException();
        }

        return true;
    }

    /**
     * Checks if the form is submitted.
     *
     * @param  string $field Form's submit button "name" attribute
     * @return bool true if the form is submitted, false otherwise
     */
    public function isPost($field = null)
    {
        return (null === $field) ? (bool) count($_POST) : isset($_POST[$field]);
    }

    /**
     * Gets default or pre-populated value for the provided field.
     *
     * @param string $field
     * @return mixed|null
     */
    public function getDefault($field)
    {
        return (true === isset($this->_prepopulatedData[$field])) ? $this->_prepopulatedData[$field] : null;
    }

    /**
     * Sets default or pre-populated value for the provided field.
     *
     * @param string $field
     * @param mixed $value
     */
    public function setDefault($field, $value)
    {
        $this->_prepopulatedData[$field] = $value;
    }

    /**
     * Gets field value.
     *
     * @param  string $field Field name
     * @return mixed string or array
     */
    public function get($field)
    {
        if (true === isset($_POST[$field]))
        {
            return $_POST[$field];
        }

        if (true === isset($this->_prepopulatedData[$field]))
        {
            return $this->_prepopulatedData[$field];
        }

        return null;
    }

    /**
     * Sets a value for a field.
     *
     * @param string $field
     * @param mixed  $value
     */
    public function set($field, $value)
    {
        $_POST[$field] = $value;
    }

    /**
     * Sets value that can be persisted via multi separate pages
     * that does not related on POST basis. E.x: redirecting
     *
     * @param string $field
     * @param string $value
     */
    public function setFlash($field, $value)
    {
        if (session_id() === '')
        {
            session_start();
        }

        $_SESSION[__CLASS__]['__flash__'][$field] = $value;
        session_commit();
    }

    /**
     * Gets cross request value and delete it.
     *
     * @see    SpicaFormService#setFlash()
     * @param  string $field
     * @return string
     */
    public function getFlash($field)
    {
        if (true === isset($_SESSION[__CLASS__]['__flash__'][$field]))
        {
            $value = $_SESSION[__CLASS__]['__flash__'][$field];
            unset($_SESSION[__CLASS__]['__flash__'][$field]);
            return $value;
        }

        return null;
    }

    /**
     * Gets the first violation message and put it into a formatted string.
     *
     * @param  string $fieldName
     * @param  string $format HTML formated string
     * @return string
     */
    public function feedback($fieldName, $format = '<div class="error">%s</div>')
    {
        if (true === empty($format))
        {
            $format = '%s';
        }

        if (true === isset($this->_violation[$fieldName]))
        {
            // Get the first validator off the stack
            $validator = reset($this->_violation[$fieldName]);
            return sprintf($format, $validator->getViolationMessage());
        }

        return null;
    }

    /**
     * Gets all violation messages.
     *
     * @param  string $field Field name
     * @return array
     */
    public function feedbacks($field)
    {
        if (true === isset($this->_violation[$field]))
        {
            $messages = array();

            foreach ($this->_violation[$field] as $validator)
            {
                $messages[] = $validator->getViolationMessage();
            }

            return $messages;
        }

        return array();
    }

    /**
     * Adds a notice to the form. When it is set, the form will have a set of notices
     * that can be used to render under the form title.
     *
     * @param  string $notice Notice message must be not empty or it will not be set into the form
     */
    public function addNotice($notice)
    {
        if ('' !== trim($notice))
        {
            $this->_notices[] = $notice;
        }
    }

    /**
     * Outputs notice messages in HTML or JSON.
     *
     * @param  string $type It takes 2 values: html and json
     * @return string A null value is returned when there is no notice
     */
    public function outputNotices($type = 'html')
    {
        switch ($type)
        {
            case 'json':
                if (false === isset($this->_notices[0]))
                {
                    return null;
                }

                return json_encode($this->_notices);

            case 'html':
            default:
                $text = null;

                if (true   === isset($this->_notices[0]))
                {
                    $text .= '<ul><li>'.implode('</li><li>', $this->_notices).'</li></ul>';
                }

                return $text;
        }
    }

    /**
     * Gets form notice messages.
     *
     * @return array A list of messages
     */
    public function getNotices()
    {
        return $this->_notices;
    }

    /**
     * Checks if the form contains any notice.
     *
     * @return bool
     */
    public function hasNotice()
    {
        return isset($this->_notices[0]);
    }

    /**
     * Sets a title to the form. When it is set, the form will have a header
     * element as the first element in the form
     *
     * @param  string $message
     */
    public function setTitle($message)
    {
        $this->_title = $message;
    }

    /**
     * Gets the form title. It should be used as the first element in the form.
     *
     * @return string form title that is already set
     */
    public function getTitle()
    {
        return $this->_title;
    }

    /**
     * Sets a form guide text block to the form. When it is set, the form will
     * have a form guide text block rendering under form title in the form.
     *
     * @param  string $message
     */
    public function setGuide($message)
    {
        $this->_guide = $message;
    }

    /**
     * Gets the form guide text block. It should be rendered under the form title.
     *
     * @return string form title that is already set
     */
    public function getGuide()
    {
        return $this->_guide;
    }

    /**
     * Gets form feedbacks as a string.
     *
     * @param  string $separator The separator used to separate each feedback message
     * @return string|null
     */
    public function getFormFeedbacksAsString($separator = ', ')
    {
        if (true === $this->hasViolationMessage())
        {
            return implode($separator, $this->_violation);
        }

        return null;
    }

    /**
     * Checks if the form has any violation message.
     *
     * @return bool
     */
    public function hasViolationMessage()
    {
        return isset($this->_violation[$i]);
    }

    /**
     * Gets violation head.
     *
     * @return string
     */
    public function getViolationHead()
    {
        return $this->_violationHead;
    }

    /**
     * Returns open form tag.
     *
     * @param array Open form tag attribute in an array
     *              Conventional indices:
     *              + class: Css class attribute
     *              + id:    Css id attribute
     *              + name:  form's name attribute
     *              + more:  more html string such as javascript
     * @return string
     */
    public function tagBegin($attribute = null)
    {
        $class = (true === isset($attribute['class']))?' class="'.$attribute['class'].'"':null;
        $id    = (true === isset($attribute['id']))?' id="'.$attribute['id'].'"':null;
        $name  = (true === isset($attribute['name']))?' name="'.$attribute['name'].'"':'poneForm';
        $more  = (true === isset($attribute['more']))?' '.$attribute['more']:null;

        return '<form'.$name.$id.$class.' accept-charset="'.$this->_acceptCharset.'" enctype="'.$this->_enctype.'" method="'.$this->_method.'" action="'.$this->_actionPath.'">';
    }

    /**
     * Returns end form tag.
     *
     * @return string
     */
    public function tagEnd()
    {
        return '</form>';
    }

    /**
     * Sets form action attribute value.
     *
     * @param string $actionPath From action attribute value
     */
    public function setActionPath($actionPath)
    {
        $this->_actionPath = $actionPath;
    }

    /**
     * Gets the form's action attribute value.
     *
     * @return string
     */
    public function getActionPath()
    {
        return $this->_actionPath;
    }

    /**
     * Sets form's enctype attribute value.
     *
     * @param string $enctype
     */
    public function setEnctype($enctype)
    {
        return $this->_enctype;
    }

    /**
     * Get form's enctype attribute value.
     *
     * @return string
     */
    public function getEnctype()
    {
        return $this->_enctype;
    }

    /**
     * Sets form's method attribute value.
     *
     * @param string $enctype
     */
    public function setMethod($enctype)
    {
        return $this->_method;
    }

    /**
     * Gets form's method attribute value.
     *
     * @return string
     */
    public function getMethod()
    {
        return $this->_method;
    }

    /**
     * Sets form's accept-charset attribute.
     * This will eliminate some charset related ambiguity
     *
     * @param string $charset
     */
    public function setAcceptCharset($charset)
    {
        $this->_acceptCharset = $charset;
    }

    /**
     * Gets form's accept-charset attribute value.
     *
     * @return string
     */
    public function getAcceptCharset()
    {
        return $this->_acceptCharset;
    }

    /**
     * Enables or disables CSRF protection.
     *
     * @param boolean $mode
     */
    public function setCSRFProtection($mode)
    {
        $this->_csrfProtectionEnabled = (bool) $mode;
    }

    /**
     * Generates a hidden security token input to prevent CSRF exploits
     *
     * This method should be called in template files
     *
     * @see    SpicaFormService::validateSecurityToken
     * @return string
     */
    public function generateSecurityToken($createNew = false)
    {
        if (null === session_id())
        {
            session_start();
        }

        if (true === $createNew || false === isset($_SESSION[self::SECURE_SESSION_KEY]))
        {
            $_SESSION[self::SECURE_SESSION_KEY] = md5(time().uniqid());
        }
    }

    /**
     * Returns html code for security hidden input.
     *
     * @return string
     */
    public function getSecurityHiddenInput()
    {
        if (false === isset($_SESSION[self::SECURE_SESSION_KEY]))
        {
            throw new SpicaException('Security token must be generated first.');
        }

        return sprintf('<input type="hidden" name="'.self::SECURE_INPUT_NAME.'" value="%s" />', $_SESSION[self::SECURE_SESSION_KEY]);
    }

    /**
     * Checks if the form is submitted with the correct security token.
     *
     * @throws Exception When secure session key is not set
     * @see    SpicaFormService::generateSecurityToken
     * @return bool
     */
    public function validateSecurityToken()
    {
        if (null  === session_id())
        {
            session_start();
        }

        if (false === isset($_SESSION[self::SECURE_SESSION_KEY]))
        {
            throw new Exception('Security token must be generated first.');
        }

        $token = (string) $_SESSION[self::SECURE_SESSION_KEY];

        // Remove the current security token
        unset($_SESSION[self::SECURE_SESSION_KEY]);

        if ($this->get(self::SECURE_INPUT_NAME) === $token)
        {
            return true;
        }

        return false;
    }

    /**
     * Checks if this form is used in update context
     *
     * @return bool
     */
    public function isUpdateContext()
    {
        return $this->_updateContext;
    }

    /**
     * Sets "processed" status
     *
     * @param bool $mode
     */
    public function setProcessed($mode)
    {
        $this->_processed = (bool) $mode;
    }

    /**
     * Is the form "processed" successfully?
     *
     * @return bool
     */
    public function isProcessed()
    {
        return $this->_processed;
    }

    /**
     * Populates form elements with data retrieved from the row set
     *
     * @param SpicaRowList $rowSet
     * @param array $map Specs: [HTML name attribute => table field name]
     */
    protected function _populateWithRowSet($rowSet, $map)
    {
        $elements = array_keys($map);
        $fields   = array_values($map);

        for ($i   = 0, $length = count($elements); $i < $length; $i++)
        {
            $this->_prepopulatedData[$elements[$i]] = $rowSet->get($fields[$i]);
        }
    }
}

/**
 * This class represents a form service that contains multiple processing status.
 *
 * namespace spica\core\service\MultipleStateFormService
 *
 * @category   spica
 * @package    core
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.3
 * @since      April 20, 2009
 * @copyright  Pham Cong Dinh (http://www.phpvietnam.net)
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: FormService.php 1869 2011-01-07 18:55:25Z pcdinh $
 */
abstract class SpicaMultipleStateForm extends SpicaFormService
{
    /**
     * Form processing state code.
     *
     * @var int
     */
    protected $_processingState = 0;

    /**
     * (non-PHPdoc)
     * @see trunk/library/spica/core/SpicaFormService#setProcessed()
     */
    public function setProcessed($mode)
    {
        throw new BadMethodCallException('Method named setProcessed() has no use in SpicaMultipleStateForm. Use setState() instead. ');
    }

    /**
     * (non-PHPdoc)
     * @see trunk/library/spica/core/SpicaFormService#isProcessed()
     */
    public function isProcessed()
    {
        throw new BadMethodCallException('Method named isProcessed() has no use in SpicaMultipleStateForm. Use getState() instead. ');
    }

    /**
     * Sets form processing state code.
     *
     * @param int $state
     */
    public function setState($state)
    {
        $this->_processingState = (int) $state;
    }

    /**
     * Gets form processing state.
     *
     * @return int
     */
    public function getState()
    {
        return $this->_processingState;
    }
}

/**
 * Set of methods considering manipulations of multiple-page form submissions
 * and user input validation.
 *
 * Form is part of domain tier.
 *
 * namespace spica\core\MultiplePageFormService
 *
 * @category   spica
 * @package    core
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.3
 * @since      April 06, 2009
 * @copyright  Pham Cong Dinh (http://www.phpvietnam.net)
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: FormService.php 1869 2011-01-07 18:55:25Z pcdinh $
 */
abstract class SpicaMultiplePageFormService extends SpicaFormService
{
    /**
     * The form's current step name.
     *
     * @var string
     */
    protected $_stepName;

    /**
     * The processed status of each form step.
     *
     * @var array
     */
    protected $_processed = array();

    /**
     * Sets step name.
     *
     * @param string $name
     */
    public function setStepName($name)
    {
        $this->_stepName = (string) $name;
    }

    /**
     * Gets step name.
     *
     * @return string
     */
    public function getStepName()
    {
        return $this->_stepName;
    }

    /**
     * Sets "processed" status
     *
     * @param bool   $mode
     * @param string $name
     */
    public function setProcessed($mode, $name = null)
    {
        $this->_processed[$name] = (bool) $mode;
    }

    /**
     * Is the form "processed" successfully?
     *
     * @param  bool $name
     * @return bool
     */
    public function isProcessed($name = null)
    {
        return $this->_processed[$name];
    }

    /**
     * Gets next step name.
     *
     * @return string
     */
    public function getNextStepName()
    {

    }
}

?>